﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using Sundstrom.Mvvm.Validation;
using Sundstrom.Validation;

namespace Sundstrom.Mvvm
{
    /// <summary>
    ///     An extended view model with functionality for validation. 
    ///     Based on code from http://blog.micic.ch/net/easy-mvvm-example-with-inotifypropertychanged-and-inotifydataerrorinfo
    ///     with small modifications: Added CallerMemberNameAttribute. Changed access modifiers.
    /// </summary>
    public abstract class ExtendedViewModelBase : ViewModelBase, INotifyDataErrorInfo
    {
        protected static readonly string DefaultValidationErrorMessage = "Validation for property \"{0}\" failed.";

        public ExtendedViewModelBase()
        {
            SubViewModels = new List<ExtendedViewModelBase>();

            GetAllErrorsOnNull = false;
            ValidationErrorMessageFactory = (propertyName) =>
            {
                return string.Format(DefaultValidationErrorMessage, propertyName);
            };
            ValidationMethodByConvention = true;
        }

        /// <summary>
        /// Gets or sets the value indicating whether all errors should be returned when null is passed to GetErrors.
        /// </summary>
        /// <returns></returns>
        protected bool GetAllErrorsOnNull { get; set; }

        #region Notify data error

        private readonly Dictionary<string, List<string>> _errors = new Dictionary<string, List<string>>();

        /// <summary>
        /// Gets a value that indicates whether or not the view model is valid.
        /// </summary>
        public bool IsValid
        {
            get { return !HasErrors; }
        }

        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

        /// <summary>
        /// Gets the errors for a specified property. 
        /// </summary>
        /// <param name="propertyName">The name of the property.</param>
        /// <returns></returns>
        public IEnumerable GetErrors(string propertyName)
        {
            //Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(propertyName));

            if (string.IsNullOrEmpty(propertyName))
            {
                if (GetAllErrorsOnNull)
                    return (_errors.Values);

                return null;
            }

            if (_errors.ContainsKey(propertyName))
            {
                return _errors[propertyName];
            }

            return null;
        }

        /// <summary>
        /// Gets a value that indicates whether or not the view model has any errors.
        /// </summary>
        public bool HasErrors
        {
            get
            {
                bool result = false;

                foreach (var viewModel in SubViewModels)
                {
                    result |= viewModel.HasErrors;
                }

                return (_errors.Count > 0) || result;
            }
        }

        /// <summary>
        /// Gets or sets the factory method that produces the default validation error message.
        /// </summary>
        /// <returns></returns>
        protected Func<object, string> ValidationErrorMessageFactory { get; set; }

        /// <summary>
        /// Gets or sets a value indicating whether validation methods should be picked by convention or not.
        /// </summary>
        /// <returns></returns>
        protected bool ValidationMethodByConvention { get; set; }

        // object is valid

        /// <summary>
        /// Adds an error for a specified property.
        /// </summary>
        /// <param name="error">The error message.</param>
        /// <param name="propertyName">The name of the property.</param>
        protected void AddError(string error, [CallerMemberName] string propertyName = null)
        {
            Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(error));
            Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(propertyName));

            if (((ISupportingMethods)this).HasProperty(propertyName))
            {
                // Add error to list
                _errors[propertyName] = new List<string> { error };
                NotifyErrorsChanged(propertyName);

                RaisePropertyChanged("IsValid");
                RaisePropertyChanged("HasErrors");
            }
            else
            {
                throw new ArgumentException(string.Format(PropertyNotAMember, GetType().FullName), "propertyName");
            }
        }

        /// <summary>
        /// Adds an error for a specified property.
        /// </summary>
        /// <typeparam name="TProperty">The type of the property.</typeparam>
        /// <param name="propertyExpression">An expression.</param>
        /// <param name="error">The error text.</param>
        protected void AddError<TProperty>(Expression<Func<TProperty>> propertyExpression, string error)
        {
            Contract.Requires<ArgumentException>(propertyExpression != null);
            Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(error));

            var body = propertyExpression.Body;
            var memberExpression = body as MemberExpression;
            if (memberExpression != null && memberExpression.Member is PropertyInfo)
            {
                var member = memberExpression.Member;
                try
                {
                    AddError(error, member.Name);
                }
                catch (ArgumentException e)
                {
                    throw new ArgumentException(e.Message, "propertyExpression");
                }
            }
            else
            {
                throw new ArgumentException(PropertyExpressionExpected, "propertyExpression");
            }
        }

        /// <summary>
        /// Removes the errors for a specified property.
        /// </summary>
        /// <param name="propertyName">The name of the property.</param>
        protected void RemoveError([CallerMemberName] string propertyName = null)
        {
            Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(propertyName));

            if (((ISupportingMethods)this).HasProperty(propertyName))
            {
                // remove error
                if (_errors.ContainsKey(propertyName))
                    _errors.Remove(propertyName);
                NotifyErrorsChanged(propertyName);

                RaisePropertyChanged("IsValid");
                RaisePropertyChanged("HasErrors");
            }
            else
            {
                throw new ArgumentException(string.Format(PropertyNotAMember, GetType().FullName), "propertyName");
            }
        }

        /// <summary>
        /// Removes the errors for a specified property.
        /// </summary>
        /// <typeparam name="TProperty">The type of the property.</typeparam>
        /// <param name="propertyExpression">An expression.</param>
        protected void RemoveError<TProperty>(Expression<Func<TProperty>> propertyExpression)
        {
            Contract.Requires<ArgumentException>(propertyExpression != null);

            var body = propertyExpression.Body;
            var memberExpression = body as MemberExpression;
            if (memberExpression != null && memberExpression.Member is PropertyInfo)
            {
                var member = memberExpression.Member;
                try
                {
                    RemoveError(member.Name);
                }
                catch (ArgumentException e)
                {
                    throw new ArgumentException(e.Message, "propertyExpression");
                }
            }
            else
            {
                throw new ArgumentException(PropertyExpressionExpected, "propertyExpression");
            }
        }

        /// <summary>
        /// Notifies that collection of errors have changed for a specified property.
        /// </summary>
        /// <param name="propertyName">The name of the property.</param>
        private void NotifyErrorsChanged([CallerMemberName] string propertyName = null)
        {
            Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(propertyName));

            // Notify
            if (ErrorsChanged != null)
                ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
        }

        #endregion

        /// <summary>
        /// Validates a property.
        /// </summary>
        /// <param name="propertyExpression">An expression for a property.</param>
        /// <typeparam name="TProperty">The type of the property in the expression.</typeparam>
        protected void ValidateProperty<TProperty>(Expression<Func<TProperty>> propertyExpression)
        {
            Contract.Requires<ArgumentException>(propertyExpression != null);

            var body = propertyExpression.Body;
            var memberExpression = body as MemberExpression;
            if (memberExpression != null && memberExpression.Member is PropertyInfo)
            {
                var member = memberExpression.Member;
                try
                {
                    ValidateProperty(member.Name);
                }
                catch (ArgumentException e)
                {
                    throw new ArgumentException(e.Message, "propertyExpression");
                }
            }
            else
            {
                throw new ArgumentException(PropertyExpressionExpected, "propertyExpression");
            }
        }

        /// <summary>
        /// Validates a property.
        /// </summary>
        /// <param name="propertyName">The name of the property.</param>
        public void ValidateProperty([CallerMemberName] string propertyName = null)
        {
            Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(propertyName));

            if (((ISupportingMethods)this).HasProperty(propertyName))
            {
#if NET45
                var propertyInfo = GetType().GetRuntimeProperty(propertyName);
#else
                var propertyInfo = GetType().GetProperty(propertyName);
#endif
                object value = propertyInfo.GetValue(this, null);

                var results = new List<ValidationResult>();

                RemoveError(propertyName);

                if (HasValidationAttribute(propertyInfo))
                {
                    if (!DataAnnotationValidator.ValidateProperty(this, propertyName, value, out results))
                    {
                        foreach (var validationResult in results)
                        {
                            AddError(
                                validationResult.ErrorMessage,
                                propertyName);
                        }
                    }
                }

                MethodInfo[] validationMethods = GetValidationMethods(propertyName);
                foreach (var validationMethod in validationMethods)
                {
                    object result = null;

                    var parameters = validationMethod.GetParameters();
                    if (parameters.Count() == 0)
                    {
                        result = validationMethod.Invoke(this, null);
                    }
                    else if (parameters.Count() == 1 && parameters.ElementAt(0).ParameterType == typeof(ValidationContext))
                    {
                        var validationContext = new ValidationContext(this, propertyName, null);
                        result = validationMethod.Invoke(this, new[] { validationContext });
                    }
                    else if (parameters.Count() == 1 && parameters.ElementAt(0).ParameterType == typeof(string))
                    {
                        result = validationMethod.Invoke(this, new[] { propertyName });
                    }
                    else
                    {
                        throw new ArgumentException("Invalid parameters defined for method \"{0}\".", validationMethod.Name);
                    }

                    if (validationMethod.ReturnType == typeof(ValidationResult))
                    {
                        var validationResult = result as ValidationResult;

                        if (validationResult != null)
                        {
                            AddError(
                                  validationResult.ErrorMessage,
                                  propertyName);
                        }
                    }
                    else if (validationMethod.ReturnType == typeof(bool))
                    {
                        if ((bool)result == false)
                        {
                            AddError(
                              string.Format(ValidationErrorMessageFactory(propertyName), propertyName),
                              propertyName);
                        }
                    }
                    else if (validationMethod.ReturnType == typeof(void))
                    {
                        // Handled manually.
                    }
                    else
                    {
                        throw new InvalidCastException(string.Format("Method \"{0}\" expected to return either type ValidationResult, System.Boolean or void.", validationMethod));
                    }
                }
            }
            else
            {
                throw new ArgumentException(string.Format(PropertyNotAMember, GetType().FullName), "propertyName");
            }
        }

        private MethodInfo[] GetValidationMethods(string propertyName)
        {
            List<MethodInfo> validationMethods = new List<MethodInfo>();
#if NET45

            var methods = GetType().GetTypeInfo().DeclaredMethods.Where(x =>
            {
                var attribute = x.GetCustomAttribute<ValidationMethodAttribute>();
                if (attribute == null) return false;

                return attribute.PropertyName == propertyName;
            });

            validationMethods.AddRange(methods);

            if (ValidationMethodByConvention)
            {
                methods = GetType().GetTypeInfo().DeclaredMethods
                    .Where(x =>
                {
                    var r = x.Name == string.Format("Validate{0}", propertyName)
                          && !x.GetParameters().Any();

                    if (r)
                    {
                        var attribute = x.GetCustomAttribute<NotValidationMethodAttribute>();
                        if (attribute != null) return false;
                    }

                    return r;
                });

                validationMethods.AddRange(methods);
            }
#else

            var methods = GetType().GetMethods().Where(x => {
                var attribute = x.GetCustomAttributes(typeof(ValidationMethodAttribute), true)
                    .Cast<ValidationMethodAttribute>().FirstOrDefault();
                if (attribute == null) return false;

                return attribute.PropertyName == propertyName;
            });

            validationMethods.AddRange(methods);

            if (ValidationMethodByConvention)
            {
                methods = GetType().GetMethods()
                           .Where(x => {

                    var r = x.Name == string.Format("Validate{0}", propertyName)
                          && !x.GetParameters().Any();

                    if(r)
                    {
                                   var attribute = x.GetCustomAttributes(typeof(NotValidationMethodAttribute), true)
                                        .Cast<NotValidationMethodAttribute>().FirstOrDefault();
                                   if (attribute != null) return false;
                    }

                    return r;
                });

                validationMethods.AddRange(methods);
            }
#endif
            return validationMethods.ToArray();
        }

        private bool HasValidationAttribute(PropertyInfo propertyInfo)
        {
            IEnumerable<Attribute> attributes = null;

#if NET45
            attributes = propertyInfo.GetCustomAttributes();
            return attributes.Any(a => a.GetType().GetTypeInfo()
                .IsSubclassOf(typeof(ValidationAttribute)));
#else
            attributes = propertyInfo.GetCustomAttributes(true).Cast<Attribute>();
            return attributes.Any(a => a.GetType()
                .IsSubclassOf(typeof(ValidationAttribute)));
#endif
        }

        /// <summary>
        /// Validates the view model.
        /// </summary>
        /// <returns>A value that indicates whether or not the view model is valid.</returns>
        public virtual bool Validate()
        {
            validate();

            foreach (var subViewModel in SubViewModels)
            {
                subViewModel.Validate();
            }

            return IsValid;
        }

        /// <summary>
        /// Validates the view model.
        /// </summary>
        /// <returns>A value that indicates whether or not the view model is valid.</returns>
        public virtual bool Validate(string group)
        {
            validate(group);

            foreach (var subViewModel in SubViewModels)
            {
                subViewModel.Validate(group);
            }

            return IsValid;
        }

        private void validate()
        {
#if NET45
            IEnumerable<PropertyInfo> props = GetType().GetRuntimeProperties();
#else
            var props = GetType().GetProperties();
#endif

            foreach (PropertyInfo prop in props)
            {
#if NET45
                if (!prop.GetMethod.IsPublic)
                    continue;
#else
                if (!prop.GetGetMethod().IsPublic)
                    continue;
#endif

                ValidateProperty(prop.Name);
            }
        }

        private void validate(string group)
        {
            if (string.IsNullOrEmpty(group))
                throw new ArgumentException(group);

#if NET45
            IEnumerable<PropertyInfo> props = GetType().GetRuntimeProperties();
#else
            var props = GetType().GetProperties();
#endif

            foreach (PropertyInfo prop in props)
            {
#if NET45
                if (!prop.GetMethod.IsPublic)
                    continue;
#else
                if (!prop.GetGetMethod().IsPublic)
                    continue;
#endif

                if (prop
                    .GetCustomAttributes(typeof(ValidationGroupAttribute), true)
                    .Cast<ValidationGroupAttribute>()
                    .Any(x => x.Group == group))
                {
                    ValidateProperty(prop.Name);
                }
            }
        }

        /// <summary>
        /// Clear all errors.
        /// </summary>
        public void ClearErrors()
        {
            _errors.Clear();

            foreach (var subViewModel in SubViewModels)
            {
                subViewModel.ClearErrors();
            }
        }

        /// <summary>
        /// Clear all errors.
        /// </summary>
        public void ClearErrors(string group)
        {
#if NET45
            IEnumerable<PropertyInfo> props = GetType().GetRuntimeProperties();
#else
            var props = GetType().GetProperties();
#endif

            foreach (PropertyInfo prop in props)
            {
#if NET45
                if (!prop.GetMethod.IsPublic)
                    continue;
#else
                if (!prop.GetGetMethod().IsPublic)
                    continue;
#endif

                if (prop
                    .GetCustomAttributes(typeof(ValidationGroupAttribute), true)
                    .Cast<ValidationGroupAttribute>()
                    .Any(x => x.Group == group))
                {
                    _errors.Remove(prop.Name);
                }
            }

            foreach (var subViewModel in SubViewModels)
            {
                subViewModel.ClearErrors(group);
            }
        }

        /// <summary>
        /// Gets a list of all registered sub view-models.
        /// </summary>
        /// <returns></returns>
        protected List<ExtendedViewModelBase> SubViewModels { get; private set; }

        /// <summary>
        /// Gets all errors. (including those in sub view models)
        /// </summary>
        /// <returns></returns>
        public IDictionary<string, IEnumerable<string>> GetErrors()
        {
            var list = new Dictionary<string, IEnumerable<string>>();

            foreach (var keyValuePair in _errors)
            {
                list[keyValuePair.Key] = keyValuePair.Value;
            }

            foreach (var subViewModel in SubViewModels)
            {
                foreach (var keyValuePair in subViewModel.GetErrors())
                {
                    list[keyValuePair.Key] = keyValuePair.Value;
                }
            }

            return list;
        }
    }
}